/**
 * messageformat.js
 *
 * ICU PluralFormat + SelectFormat for JavaScript
 *
 * @author Alex Sexton - @SlexAxton
 * @version 0.1.4
 * @license WTFPL
 * @contributor_license Dojo CLA
*/
(function ( root ) {

  // Create the contructor function
  function MessageFormat ( locale, pluralFunc ) {
    var fallbackLocale;

    if ( locale && pluralFunc ) {
      MessageFormat.locale[ locale ] = pluralFunc;
    }

    // Defaults
    fallbackLocale = locale = locale || "en";
    pluralFunc = pluralFunc || MessageFormat.locale[ fallbackLocale = MessageFormat.Utils.getFallbackLocale( locale ) ];

    if ( ! pluralFunc ) {
      throw new Error( "Plural Function not found for locale: " + locale );
    }

    // Own Properties
    this.pluralFunc = pluralFunc;
    this.locale = locale;
    this.fallbackLocale = fallbackLocale;
  }

  // Set up the locales object. Add in english by default
  MessageFormat.locale = {
    "en" : function ( n ) {
      if ( n === 1 ) {
        return "one";
      }
      return "other";
    }
  };

  // Build out our basic SafeString type
  // more or less stolen from Handlebars by @wycats
  MessageFormat.SafeString = function( string ) {
    this.string = string;
  };

  MessageFormat.SafeString.prototype.toString = function () {
    return this.string.toString();
  };

  MessageFormat.Utils = {
    numSub : function ( string, key, depth ) {
      // make sure that it's not an escaped octothorpe
      return string.replace( /^#|[^\\]#/g, function (m) {
        var prefix = m && m.length === 2 ? m.charAt(0) : '';
        return prefix + '" + (function(){ var x = ' +
        key+';\nif( isNaN(x) ){\nthrow new Error("MessageFormat: `"+lastkey_'+depth+'+"` isnt a number.");\n}\nreturn x;\n})() + "'
      });
    },
    escapeExpression : function (string) {
      var escape = {
            "\n": "\\n",
            "\"": '\\"'
          },
          badChars = /[\n"]/g,
          possible = /[\n"]/,
          escapeChar = function(chr) {
            return escape[chr] || "&amp;";
          };

      // Don't escape SafeStrings, since they're already safe
      if ( string instanceof MessageFormat.SafeString ) {
        return string.toString();
      }
      else if ( string === null || string === false ) {
        return "";
      }

      if ( ! possible.test( string ) ) {
        return string;
      }
      return string.replace( badChars, escapeChar );
    },
    getFallbackLocale: function( locale ) {
      var tagSeparator = locale.indexOf("-") >= 0 ? "-" : "_";

      // Lets just be friends, fallback through the language tags
      while ( ! MessageFormat.locale.hasOwnProperty( locale ) ) {
        locale = locale.substring(0, locale.lastIndexOf( tagSeparator ));
        if (locale.length === 0) {
          return null;
        }
      }

      return locale;
    }
  };

  // This is generated and pulled in for browsers.
  var mparser = (function(){
    /* Generated by PEG.js 0.6.2 (http://pegjs.majda.cz/). */

    var result = {
      /*
      * Parses the input with a generated parser. If the parsing is successfull,
      * returns a value explicitly or implicitly specified by the grammar from
      * which the parser was generated (see |PEG.buildParser|). If the parsing is
      * unsuccessful, throws |PEG.parser.SyntaxError| describing the error.
      */
      parse: function(input, startRule) {
        var parseFunctions = {
          "_": parse__,
          "char": parse_char,
          "chars": parse_chars,
          "digits": parse_digits,
          "elementFormat": parse_elementFormat,
          "hexDigit": parse_hexDigit,
          "id": parse_id,
          "messageFormatElement": parse_messageFormatElement,
          "messageFormatPattern": parse_messageFormatPattern,
          "messageFormatPatternRight": parse_messageFormatPatternRight,
          "offsetPattern": parse_offsetPattern,
          "pluralFormatPattern": parse_pluralFormatPattern,
          "pluralForms": parse_pluralForms,
          "pluralStyle": parse_pluralStyle,
          "selectFormatPattern": parse_selectFormatPattern,
          "selectStyle": parse_selectStyle,
          "start": parse_start,
          "string": parse_string,
          "stringKey": parse_stringKey,
          "whitespace": parse_whitespace
        };

        if (startRule !== undefined) {
          if (parseFunctions[startRule] === undefined) {
            throw new Error("Invalid rule name: " + quote(startRule) + ".");
          }
        } else {
          startRule = "start";
        }

        var pos = 0;
        var reportMatchFailures = true;
        var rightmostMatchFailuresPos = 0;
        var rightmostMatchFailuresExpected = [];
        var cache = {};

        function padLeft(input, padding, length) {
          var result = input;

          var padLength = length - input.length;
          for (var i = 0; i < padLength; i++) {
            result = padding + result;
          }

          return result;
        }

        function escape(ch) {
          var charCode = ch.charCodeAt(0);

          if (charCode <= 0xFF) {
            var escapeChar = 'x';
            var length = 2;
          } else {
            var escapeChar = 'u';
            var length = 4;
          }

          return '\\' + escapeChar + padLeft(charCode.toString(16).toUpperCase(), '0', length);
        }

        function quote(s) {
          /*
          * ECMA-262, 5th ed., 7.8.4: All characters may appear literally in a
          * string literal except for the closing quote character, backslash,
          * carriage return, line separator, paragraph separator, and line feed.
          * Any character may appear in the form of an escape sequence.
          */
          return '"' + s
          .replace(/\\/g, '\\\\')            // backslash
          .replace(/"/g, '\\"')              // closing quote character
          .replace(/\r/g, '\\r')             // carriage return
          .replace(/\n/g, '\\n')             // line feed
          .replace(/[\x80-\uFFFF]/g, escape) // non-ASCII characters
          + '"';
        }

        function matchFailed(failure) {
          if (pos < rightmostMatchFailuresPos) {
            return;
          }

          if (pos > rightmostMatchFailuresPos) {
            rightmostMatchFailuresPos = pos;
            rightmostMatchFailuresExpected = [];
          }

          rightmostMatchFailuresExpected.push(failure);
        }

        function parse_start() {
          var cacheKey = 'start@' + pos;
          var cachedResult = cache[cacheKey];
          if (cachedResult) {
            pos = cachedResult.nextPos;
            return cachedResult.result;
          }


          var savedPos0 = pos;
          var result1 = parse_messageFormatPattern();
          var result2 = result1 !== null
          ? (function(messageFormatPattern) { return { type: "program", program: messageFormatPattern }; })(result1)
          : null;
          if (result2 !== null) {
            var result0 = result2;
          } else {
            var result0 = null;
            pos = savedPos0;
          }



          cache[cacheKey] = {
            nextPos: pos,
            result:  result0
          };
          return result0;
        }

        function parse_messageFormatPattern() {
          var cacheKey = 'messageFormatPattern@' + pos;
          var cachedResult = cache[cacheKey];
          if (cachedResult) {
            pos = cachedResult.nextPos;
            return cachedResult.result;
          }


          var savedPos0 = pos;
          var savedPos1 = pos;
          var result3 = parse_string();
          if (result3 !== null) {
            var result4 = [];
            var result5 = parse_messageFormatPatternRight();
            while (result5 !== null) {
              result4.push(result5);
              var result5 = parse_messageFormatPatternRight();
            }
            if (result4 !== null) {
              var result1 = [result3, result4];
            } else {
              var result1 = null;
              pos = savedPos1;
            }
          } else {
            var result1 = null;
            pos = savedPos1;
          }
          var result2 = result1 !== null
          ? (function(s1, inner) {
            var st = [];
            if ( s1 && s1.val ) {
              st.push( s1 );
            }
            for( var i in inner ){
              if ( inner.hasOwnProperty( i ) ) {
                st.push( inner[ i ] );
              }
            }
            return { type: 'messageFormatPattern', statements: st };
          })(result1[0], result1[1])
          : null;
          if (result2 !== null) {
            var result0 = result2;
          } else {
            var result0 = null;
            pos = savedPos0;
          }



          cache[cacheKey] = {
            nextPos: pos,
            result:  result0
          };
          return result0;
        }

        function parse_messageFormatPatternRight() {
          var cacheKey = 'messageFormatPatternRight@' + pos;
          var cachedResult = cache[cacheKey];
          if (cachedResult) {
            pos = cachedResult.nextPos;
            return cachedResult.result;
          }


          var savedPos0 = pos;
          var savedPos1 = pos;
          if (input.substr(pos, 1) === "{") {
            var result3 = "{";
              pos += 1;
            } else {
              var result3 = null;
              if (reportMatchFailures) {
                matchFailed("\"{\"");
                }
              }
              if (result3 !== null) {
                var result4 = parse__();
                if (result4 !== null) {
                  var result5 = parse_messageFormatElement();
                  if (result5 !== null) {
                    var result6 = parse__();
                    if (result6 !== null) {
                      if (input.substr(pos, 1) === "}") {
                        var result7 = "}";
                        pos += 1;
                  } else {
                    var result7 = null;
                    if (reportMatchFailures) {
                      matchFailed("\"}\"");
                  }
                }
                if (result7 !== null) {
                  var result8 = parse_string();
                  if (result8 !== null) {
                    var result1 = [result3, result4, result5, result6, result7, result8];
                  } else {
                    var result1 = null;
                    pos = savedPos1;
                  }
                } else {
                  var result1 = null;
                  pos = savedPos1;
                }
              } else {
                var result1 = null;
                pos = savedPos1;
              }
            } else {
              var result1 = null;
              pos = savedPos1;
            }
          } else {
            var result1 = null;
            pos = savedPos1;
          }
          } else {
            var result1 = null;
            pos = savedPos1;
          }
          var result2 = result1 !== null
          ? (function(mfe, s1) {
            var res = [];
            if ( mfe ) {
              res.push(mfe);
            }
            if ( s1 && s1.val ) {
              res.push( s1 );
            }
            return { type: "messageFormatPatternRight", statements : res };
          })(result1[2], result1[5])
          : null;
          if (result2 !== null) {
            var result0 = result2;
          } else {
            var result0 = null;
            pos = savedPos0;
          }



          cache[cacheKey] = {
            nextPos: pos,
            result:  result0
          };
          return result0;
        }

        function parse_messageFormatElement() {
          var cacheKey = 'messageFormatElement@' + pos;
          var cachedResult = cache[cacheKey];
          if (cachedResult) {
            pos = cachedResult.nextPos;
            return cachedResult.result;
          }


          var savedPos0 = pos;
          var savedPos1 = pos;
          var result3 = parse_id();
          if (result3 !== null) {
            var savedPos2 = pos;
            if (input.substr(pos, 1) === ",") {
              var result6 = ",";
              pos += 1;
            } else {
              var result6 = null;
              if (reportMatchFailures) {
                matchFailed("\",\"");
              }
            }
            if (result6 !== null) {
              var result7 = parse_elementFormat();
              if (result7 !== null) {
                var result5 = [result6, result7];
              } else {
                var result5 = null;
                pos = savedPos2;
              }
            } else {
              var result5 = null;
              pos = savedPos2;
            }
            var result4 = result5 !== null ? result5 : '';
            if (result4 !== null) {
              var result1 = [result3, result4];
            } else {
              var result1 = null;
              pos = savedPos1;
            }
          } else {
            var result1 = null;
            pos = savedPos1;
          }
          var result2 = result1 !== null
          ? (function(argIdx, efmt) {
            var res = {
              type: "messageFormatElement",
              argumentIndex: argIdx
            };
            if ( efmt && efmt.length ) {
              res.elementFormat = efmt[1];
            }
            else {
              res.output = true;
            }
            return res;
          })(result1[0], result1[1])
          : null;
          if (result2 !== null) {
            var result0 = result2;
          } else {
            var result0 = null;
            pos = savedPos0;
          }



          cache[cacheKey] = {
            nextPos: pos,
            result:  result0
          };
          return result0;
        }

        function parse_elementFormat() {
          var cacheKey = 'elementFormat@' + pos;
          var cachedResult = cache[cacheKey];
          if (cachedResult) {
            pos = cachedResult.nextPos;
            return cachedResult.result;
          }


          var savedPos2 = pos;
          var savedPos3 = pos;
          var result14 = parse__();
          if (result14 !== null) {
            if (input.substr(pos, 6) === "plural") {
              var result15 = "plural";
              pos += 6;
            } else {
              var result15 = null;
              if (reportMatchFailures) {
                matchFailed("\"plural\"");
              }
            }
            if (result15 !== null) {
              var result16 = parse__();
              if (result16 !== null) {
                if (input.substr(pos, 1) === ",") {
                  var result17 = ",";
                  pos += 1;
                } else {
                  var result17 = null;
                  if (reportMatchFailures) {
                    matchFailed("\",\"");
                  }
                }
                if (result17 !== null) {
                  var result18 = parse__();
                  if (result18 !== null) {
                    var result19 = parse_pluralStyle();
                    if (result19 !== null) {
                      var result20 = parse__();
                      if (result20 !== null) {
                        var result12 = [result14, result15, result16, result17, result18, result19, result20];
                      } else {
                        var result12 = null;
                        pos = savedPos3;
                      }
                    } else {
                      var result12 = null;
                      pos = savedPos3;
                    }
                  } else {
                    var result12 = null;
                    pos = savedPos3;
                  }
                } else {
                  var result12 = null;
                  pos = savedPos3;
                }
              } else {
                var result12 = null;
                pos = savedPos3;
              }
            } else {
              var result12 = null;
              pos = savedPos3;
            }
          } else {
            var result12 = null;
            pos = savedPos3;
          }
          var result13 = result12 !== null
          ? (function(t, s) {
            return {
              type : "elementFormat",
              key  : t,
              val  : s.val
            };
          })(result12[1], result12[5])
          : null;
          if (result13 !== null) {
            var result11 = result13;
          } else {
            var result11 = null;
            pos = savedPos2;
          }
          if (result11 !== null) {
            var result0 = result11;
          } else {
            var savedPos0 = pos;
            var savedPos1 = pos;
            var result4 = parse__();
            if (result4 !== null) {
              if (input.substr(pos, 6) === "select") {
                var result5 = "select";
                pos += 6;
              } else {
                var result5 = null;
                if (reportMatchFailures) {
                  matchFailed("\"select\"");
                }
              }
              if (result5 !== null) {
                var result6 = parse__();
                if (result6 !== null) {
                  if (input.substr(pos, 1) === ",") {
                    var result7 = ",";
                    pos += 1;
                  } else {
                    var result7 = null;
                    if (reportMatchFailures) {
                      matchFailed("\",\"");
                    }
                  }
                  if (result7 !== null) {
                    var result8 = parse__();
                    if (result8 !== null) {
                      var result9 = parse_selectStyle();
                      if (result9 !== null) {
                        var result10 = parse__();
                        if (result10 !== null) {
                          var result2 = [result4, result5, result6, result7, result8, result9, result10];
                        } else {
                          var result2 = null;
                          pos = savedPos1;
                        }
                      } else {
                        var result2 = null;
                        pos = savedPos1;
                      }
                    } else {
                      var result2 = null;
                      pos = savedPos1;
                    }
                  } else {
                    var result2 = null;
                    pos = savedPos1;
                  }
                } else {
                  var result2 = null;
                  pos = savedPos1;
                }
              } else {
                var result2 = null;
                pos = savedPos1;
              }
            } else {
              var result2 = null;
              pos = savedPos1;
            }
            var result3 = result2 !== null
            ? (function(t, s) {
              return {
                type : "elementFormat",
                key  : t,
                val  : s.val
              };
            })(result2[1], result2[5])
            : null;
            if (result3 !== null) {
              var result1 = result3;
            } else {
              var result1 = null;
              pos = savedPos0;
            }
            if (result1 !== null) {
              var result0 = result1;
            } else {
              var result0 = null;;
            };
          }



          cache[cacheKey] = {
            nextPos: pos,
            result:  result0
          };
          return result0;
        }

        function parse_pluralStyle() {
          var cacheKey = 'pluralStyle@' + pos;
          var cachedResult = cache[cacheKey];
          if (cachedResult) {
            pos = cachedResult.nextPos;
            return cachedResult.result;
          }


          var savedPos0 = pos;
          var result1 = parse_pluralFormatPattern();
          var result2 = result1 !== null
          ? (function(pfp) {
            return { type: "pluralStyle", val: pfp };
          })(result1)
          : null;
          if (result2 !== null) {
            var result0 = result2;
          } else {
            var result0 = null;
            pos = savedPos0;
          }



          cache[cacheKey] = {
            nextPos: pos,
            result:  result0
          };
          return result0;
        }

        function parse_selectStyle() {
          var cacheKey = 'selectStyle@' + pos;
          var cachedResult = cache[cacheKey];
          if (cachedResult) {
            pos = cachedResult.nextPos;
            return cachedResult.result;
          }


          var savedPos0 = pos;
          var result1 = parse_selectFormatPattern();
          var result2 = result1 !== null
          ? (function(sfp) {
            return { type: "selectStyle", val: sfp };
          })(result1)
          : null;
          if (result2 !== null) {
            var result0 = result2;
          } else {
            var result0 = null;
            pos = savedPos0;
          }



          cache[cacheKey] = {
            nextPos: pos,
            result:  result0
          };
          return result0;
        }

        function parse_pluralFormatPattern() {
          var cacheKey = 'pluralFormatPattern@' + pos;
          var cachedResult = cache[cacheKey];
          if (cachedResult) {
            pos = cachedResult.nextPos;
            return cachedResult.result;
          }


          var savedPos0 = pos;
          var savedPos1 = pos;
          var result6 = parse_offsetPattern();
          var result3 = result6 !== null ? result6 : '';
          if (result3 !== null) {
            var result4 = [];
            var result5 = parse_pluralForms();
            while (result5 !== null) {
              result4.push(result5);
              var result5 = parse_pluralForms();
            }
            if (result4 !== null) {
              var result1 = [result3, result4];
            } else {
              var result1 = null;
              pos = savedPos1;
            }
          } else {
            var result1 = null;
            pos = savedPos1;
          }
          var result2 = result1 !== null
          ? (function(op, pf) {
            var res = {
              type: "pluralFormatPattern",
              pluralForms: pf
            };
            if ( op ) {
              res.offset = op;
            }
            else {
              res.offset = 0;
            }
            return res;
          })(result1[0], result1[1])
          : null;
          if (result2 !== null) {
            var result0 = result2;
          } else {
            var result0 = null;
            pos = savedPos0;
          }



          cache[cacheKey] = {
            nextPos: pos,
            result:  result0
          };
          return result0;
        }

        function parse_offsetPattern() {
          var cacheKey = 'offsetPattern@' + pos;
          var cachedResult = cache[cacheKey];
          if (cachedResult) {
            pos = cachedResult.nextPos;
            return cachedResult.result;
          }


          var savedPos0 = pos;
          var savedPos1 = pos;
          var result3 = parse__();
          if (result3 !== null) {
            if (input.substr(pos, 6) === "offset") {
              var result4 = "offset";
              pos += 6;
            } else {
              var result4 = null;
              if (reportMatchFailures) {
                matchFailed("\"offset\"");
              }
            }
            if (result4 !== null) {
              var result5 = parse__();
              if (result5 !== null) {
                if (input.substr(pos, 1) === ":") {
                  var result6 = ":";
                  pos += 1;
                } else {
                  var result6 = null;
                  if (reportMatchFailures) {
                    matchFailed("\":\"");
                  }
                }
                if (result6 !== null) {
                  var result7 = parse__();
                  if (result7 !== null) {
                    var result8 = parse_digits();
                    if (result8 !== null) {
                      var result9 = parse__();
                      if (result9 !== null) {
                        var result1 = [result3, result4, result5, result6, result7, result8, result9];
                      } else {
                        var result1 = null;
                        pos = savedPos1;
                      }
                    } else {
                      var result1 = null;
                      pos = savedPos1;
                    }
                  } else {
                    var result1 = null;
                    pos = savedPos1;
                  }
                } else {
                  var result1 = null;
                  pos = savedPos1;
                }
              } else {
                var result1 = null;
                pos = savedPos1;
              }
            } else {
              var result1 = null;
              pos = savedPos1;
            }
          } else {
            var result1 = null;
            pos = savedPos1;
          }
          var result2 = result1 !== null
          ? (function(d) {
            return d;
          })(result1[5])
          : null;
          if (result2 !== null) {
            var result0 = result2;
          } else {
            var result0 = null;
            pos = savedPos0;
          }



          cache[cacheKey] = {
            nextPos: pos,
            result:  result0
          };
          return result0;
        }

        function parse_selectFormatPattern() {
          var cacheKey = 'selectFormatPattern@' + pos;
          var cachedResult = cache[cacheKey];
          if (cachedResult) {
            pos = cachedResult.nextPos;
            return cachedResult.result;
          }


          var savedPos0 = pos;
          var result1 = [];
          var result3 = parse_pluralForms();
          while (result3 !== null) {
            result1.push(result3);
            var result3 = parse_pluralForms();
          }
          var result2 = result1 !== null
          ? (function(pf) {
            return {
              type: "selectFormatPattern",
              pluralForms: pf
            };
          })(result1)
          : null;
          if (result2 !== null) {
            var result0 = result2;
          } else {
            var result0 = null;
            pos = savedPos0;
          }



          cache[cacheKey] = {
            nextPos: pos,
            result:  result0
          };
          return result0;
        }

        function parse_pluralForms() {
          var cacheKey = 'pluralForms@' + pos;
          var cachedResult = cache[cacheKey];
          if (cachedResult) {
            pos = cachedResult.nextPos;
            return cachedResult.result;
          }


          var savedPos0 = pos;
          var savedPos1 = pos;
          var result3 = parse__();
          if (result3 !== null) {
            var result4 = parse_stringKey();
            if (result4 !== null) {
              var result5 = parse__();
              if (result5 !== null) {
                if (input.substr(pos, 1) === "{") {
                  var result6 = "{";
                    pos += 1;
                  } else {
                    var result6 = null;
                    if (reportMatchFailures) {
                      matchFailed("\"{\"");
                      }
                    }
                    if (result6 !== null) {
                      var result7 = parse__();
                      if (result7 !== null) {
                        var result8 = parse_messageFormatPattern();
                        if (result8 !== null) {
                          var result9 = parse__();
                          if (result9 !== null) {
                            if (input.substr(pos, 1) === "}") {
                              var result10 = "}";
                              pos += 1;
                        } else {
                          var result10 = null;
                          if (reportMatchFailures) {
                            matchFailed("\"}\"");
                        }
                      }
                      if (result10 !== null) {
                        var result1 = [result3, result4, result5, result6, result7, result8, result9, result10];
                      } else {
                        var result1 = null;
                        pos = savedPos1;
                      }
                    } else {
                      var result1 = null;
                      pos = savedPos1;
                    }
                  } else {
                    var result1 = null;
                    pos = savedPos1;
                  }
                } else {
                  var result1 = null;
                  pos = savedPos1;
                }
                } else {
                  var result1 = null;
                  pos = savedPos1;
                }
              } else {
                var result1 = null;
                pos = savedPos1;
              }
            } else {
              var result1 = null;
              pos = savedPos1;
            }
          } else {
            var result1 = null;
            pos = savedPos1;
          }
          var result2 = result1 !== null
          ? (function(k, mfp) {
            return {
              type: "pluralForms",
              key: k,
              val: mfp
            };
          })(result1[1], result1[5])
          : null;
          if (result2 !== null) {
            var result0 = result2;
          } else {
            var result0 = null;
            pos = savedPos0;
          }



          cache[cacheKey] = {
            nextPos: pos,
            result:  result0
          };
          return result0;
        }

        function parse_stringKey() {
          var cacheKey = 'stringKey@' + pos;
          var cachedResult = cache[cacheKey];
          if (cachedResult) {
            pos = cachedResult.nextPos;
            return cachedResult.result;
          }


          var savedPos2 = pos;
          var result7 = parse_id();
          var result8 = result7 !== null
          ? (function(i) {
            return i;
          })(result7)
          : null;
          if (result8 !== null) {
            var result6 = result8;
          } else {
            var result6 = null;
            pos = savedPos2;
          }
          if (result6 !== null) {
            var result0 = result6;
          } else {
            var savedPos0 = pos;
            var savedPos1 = pos;
            if (input.substr(pos, 1) === "=") {
              var result4 = "=";
              pos += 1;
            } else {
              var result4 = null;
              if (reportMatchFailures) {
                matchFailed("\"=\"");
              }
            }
            if (result4 !== null) {
              var result5 = parse_digits();
              if (result5 !== null) {
                var result2 = [result4, result5];
              } else {
                var result2 = null;
                pos = savedPos1;
              }
            } else {
              var result2 = null;
              pos = savedPos1;
            }
            var result3 = result2 !== null
            ? (function(d) {
              return d;
            })(result2[1])
            : null;
            if (result3 !== null) {
              var result1 = result3;
            } else {
              var result1 = null;
              pos = savedPos0;
            }
            if (result1 !== null) {
              var result0 = result1;
            } else {
              var result0 = null;;
            };
          }



          cache[cacheKey] = {
            nextPos: pos,
            result:  result0
          };
          return result0;
        }

        function parse_string() {
          var cacheKey = 'string@' + pos;
          var cachedResult = cache[cacheKey];
          if (cachedResult) {
            pos = cachedResult.nextPos;
            return cachedResult.result;
          }


          var savedPos0 = pos;
          var savedPos1 = pos;
          var result3 = parse__();
          if (result3 !== null) {
            var result4 = [];
            var savedPos2 = pos;
            var result6 = parse__();
            if (result6 !== null) {
              var result7 = parse_chars();
              if (result7 !== null) {
                var result8 = parse__();
                if (result8 !== null) {
                  var result5 = [result6, result7, result8];
                } else {
                  var result5 = null;
                  pos = savedPos2;
                }
              } else {
                var result5 = null;
                pos = savedPos2;
              }
            } else {
              var result5 = null;
              pos = savedPos2;
            }
            while (result5 !== null) {
              result4.push(result5);
              var savedPos2 = pos;
              var result6 = parse__();
              if (result6 !== null) {
                var result7 = parse_chars();
                if (result7 !== null) {
                  var result8 = parse__();
                  if (result8 !== null) {
                    var result5 = [result6, result7, result8];
                  } else {
                    var result5 = null;
                    pos = savedPos2;
                  }
                } else {
                  var result5 = null;
                  pos = savedPos2;
                }
              } else {
                var result5 = null;
                pos = savedPos2;
              }
            }
            if (result4 !== null) {
              var result1 = [result3, result4];
            } else {
              var result1 = null;
              pos = savedPos1;
            }
          } else {
            var result1 = null;
            pos = savedPos1;
          }
          var result2 = result1 !== null
          ? (function(ws, s) {
            var tmp = [];
            for( var i = 0; i < s.length; ++i ) {
              for( var j = 0; j < s[ i ].length; ++j ) {
                tmp.push(s[i][j]);
              }
            }
            return {
              type: "string",
              val: ws + tmp.join('')
            };
          })(result1[0], result1[1])
          : null;
          if (result2 !== null) {
            var result0 = result2;
          } else {
            var result0 = null;
            pos = savedPos0;
          }



          cache[cacheKey] = {
            nextPos: pos,
            result:  result0
          };
          return result0;
        }

        function parse_id() {
          var cacheKey = 'id@' + pos;
          var cachedResult = cache[cacheKey];
          if (cachedResult) {
            pos = cachedResult.nextPos;
            return cachedResult.result;
          }


          var savedPos0 = pos;
          var savedPos1 = pos;
          var result3 = parse__();
          if (result3 !== null) {
            if (input.substr(pos).match(/^[a-zA-Z$_]/) !== null) {
              var result4 = input.charAt(pos);
              pos++;
            } else {
              var result4 = null;
              if (reportMatchFailures) {
                matchFailed("[a-zA-Z$_]");
              }
            }
            if (result4 !== null) {
              var result5 = [];
              if (input.substr(pos).match(/^[^ 	\n\r,.+={}]/) !== null) {
                var result7 = input.charAt(pos);
                pos++;
              } else {
                var result7 = null;
                if (reportMatchFailures) {
                  matchFailed("[^ 	\\n\\r,.+={}]");
                }
              }
              while (result7 !== null) {
                result5.push(result7);
                if (input.substr(pos).match(/^[^ 	\n\r,.+={}]/) !== null) {
                  var result7 = input.charAt(pos);
                  pos++;
                } else {
                  var result7 = null;
                  if (reportMatchFailures) {
                    matchFailed("[^ 	\\n\\r,.+={}]");
                  }
                }
              }
              if (result5 !== null) {
                var result6 = parse__();
                if (result6 !== null) {
                  var result1 = [result3, result4, result5, result6];
                } else {
                  var result1 = null;
                  pos = savedPos1;
                }
              } else {
                var result1 = null;
                pos = savedPos1;
              }
            } else {
              var result1 = null;
              pos = savedPos1;
            }
          } else {
            var result1 = null;
            pos = savedPos1;
          }
          var result2 = result1 !== null
          ? (function(s1, s2) {
            return s1 + (s2 ? s2.join('') : '');
          })(result1[1], result1[2])
          : null;
          if (result2 !== null) {
            var result0 = result2;
          } else {
            var result0 = null;
            pos = savedPos0;
          }



          cache[cacheKey] = {
            nextPos: pos,
            result:  result0
          };
          return result0;
        }

        function parse_chars() {
          var cacheKey = 'chars@' + pos;
          var cachedResult = cache[cacheKey];
          if (cachedResult) {
            pos = cachedResult.nextPos;
            return cachedResult.result;
          }


          var savedPos0 = pos;
          var result3 = parse_char();
          if (result3 !== null) {
            var result1 = [];
            while (result3 !== null) {
              result1.push(result3);
              var result3 = parse_char();
            }
          } else {
            var result1 = null;
          }
          var result2 = result1 !== null
          ? (function(chars) { return chars.join(''); })(result1)
          : null;
          if (result2 !== null) {
            var result0 = result2;
          } else {
            var result0 = null;
            pos = savedPos0;
          }



          cache[cacheKey] = {
            nextPos: pos,
            result:  result0
          };
          return result0;
        }

        function parse_char() {
          var cacheKey = 'char@' + pos;
          var cachedResult = cache[cacheKey];
          if (cachedResult) {
            pos = cachedResult.nextPos;
            return cachedResult.result;
          }


          var savedPos5 = pos;
          if (input.substr(pos).match(/^[^{}\\\0- 	\n\r]/) !== null) {
            var result19 = input.charAt(pos);
            pos++;
          } else {
            var result19 = null;
            if (reportMatchFailures) {
              matchFailed("[^{}\\\\\\0- 	\\n\\r]");
            }
          }
          var result20 = result19 !== null
          ? (function(x) {
            return x;
          })(result19)
          : null;
          if (result20 !== null) {
            var result18 = result20;
          } else {
            var result18 = null;
            pos = savedPos5;
          }
          if (result18 !== null) {
            var result0 = result18;
          } else {
            var savedPos4 = pos;
            if (input.substr(pos, 2) === "\\#") {
              var result16 = "\\#";
              pos += 2;
            } else {
              var result16 = null;
              if (reportMatchFailures) {
                matchFailed("\"\\\\#\"");
              }
            }
            var result17 = result16 !== null
            ? (function() {
              return "\\#";
            })()
            : null;
            if (result17 !== null) {
              var result15 = result17;
            } else {
              var result15 = null;
              pos = savedPos4;
            }
            if (result15 !== null) {
              var result0 = result15;
            } else {
              var savedPos3 = pos;
              if (input.substr(pos, 2) === "\\{") {
                var result13 = "\\{";
                  pos += 2;
                } else {
                  var result13 = null;
                  if (reportMatchFailures) {
                    matchFailed("\"\\\\{\"");
                    }
                  }
                  var result14 = result13 !== null
                  ? (function() {
                    return "\u007B";
                  })()
                  : null;
                  if (result14 !== null) {
                    var result12 = result14;
                  } else {
                    var result12 = null;
                    pos = savedPos3;
                  }
                  if (result12 !== null) {
                    var result0 = result12;
                  } else {
                    var savedPos2 = pos;
                    if (input.substr(pos, 2) === "\\}") {
                      var result10 = "\\}";
                      pos += 2;
                } else {
                  var result10 = null;
                  if (reportMatchFailures) {
                    matchFailed("\"\\\\}\"");
                }
              }
              var result11 = result10 !== null
              ? (function() {
                return "\u007D";
              })()
              : null;
              if (result11 !== null) {
                var result9 = result11;
              } else {
                var result9 = null;
                pos = savedPos2;
              }
              if (result9 !== null) {
                var result0 = result9;
              } else {
                var savedPos0 = pos;
                var savedPos1 = pos;
                if (input.substr(pos, 2) === "\\u") {
                  var result4 = "\\u";
                  pos += 2;
                } else {
                  var result4 = null;
                  if (reportMatchFailures) {
                    matchFailed("\"\\\\u\"");
                  }
                }
                if (result4 !== null) {
                  var result5 = parse_hexDigit();
                  if (result5 !== null) {
                    var result6 = parse_hexDigit();
                    if (result6 !== null) {
                      var result7 = parse_hexDigit();
                      if (result7 !== null) {
                        var result8 = parse_hexDigit();
                        if (result8 !== null) {
                          var result2 = [result4, result5, result6, result7, result8];
                        } else {
                          var result2 = null;
                          pos = savedPos1;
                        }
                      } else {
                        var result2 = null;
                        pos = savedPos1;
                      }
                    } else {
                      var result2 = null;
                      pos = savedPos1;
                    }
                  } else {
                    var result2 = null;
                    pos = savedPos1;
                  }
                } else {
                  var result2 = null;
                  pos = savedPos1;
                }
                var result3 = result2 !== null
                ? (function(h1, h2, h3, h4) {
                  return String.fromCharCode(parseInt("0x" + h1 + h2 + h3 + h4));
                })(result2[1], result2[2], result2[3], result2[4])
                : null;
                if (result3 !== null) {
                  var result1 = result3;
                } else {
                  var result1 = null;
                  pos = savedPos0;
                }
                if (result1 !== null) {
                  var result0 = result1;
                } else {
                  var result0 = null;;
                };
              };
              };
            };
          }



          cache[cacheKey] = {
            nextPos: pos,
            result:  result0
          };
          return result0;
        }

        function parse_digits() {
          var cacheKey = 'digits@' + pos;
          var cachedResult = cache[cacheKey];
          if (cachedResult) {
            pos = cachedResult.nextPos;
            return cachedResult.result;
          }


          var savedPos0 = pos;
          if (input.substr(pos).match(/^[0-9]/) !== null) {
            var result3 = input.charAt(pos);
            pos++;
          } else {
            var result3 = null;
            if (reportMatchFailures) {
              matchFailed("[0-9]");
            }
          }
          if (result3 !== null) {
            var result1 = [];
            while (result3 !== null) {
              result1.push(result3);
              if (input.substr(pos).match(/^[0-9]/) !== null) {
                var result3 = input.charAt(pos);
                pos++;
              } else {
                var result3 = null;
                if (reportMatchFailures) {
                  matchFailed("[0-9]");
                }
              }
            }
          } else {
            var result1 = null;
          }
          var result2 = result1 !== null
          ? (function(ds) {
            return parseInt((ds.join('')), 10);
          })(result1)
          : null;
          if (result2 !== null) {
            var result0 = result2;
          } else {
            var result0 = null;
            pos = savedPos0;
          }



          cache[cacheKey] = {
            nextPos: pos,
            result:  result0
          };
          return result0;
        }

        function parse_hexDigit() {
          var cacheKey = 'hexDigit@' + pos;
          var cachedResult = cache[cacheKey];
          if (cachedResult) {
            pos = cachedResult.nextPos;
            return cachedResult.result;
          }


          if (input.substr(pos).match(/^[0-9a-fA-F]/) !== null) {
            var result0 = input.charAt(pos);
            pos++;
          } else {
            var result0 = null;
            if (reportMatchFailures) {
              matchFailed("[0-9a-fA-F]");
            }
          }



          cache[cacheKey] = {
            nextPos: pos,
            result:  result0
          };
          return result0;
        }

        function parse__() {
          var cacheKey = '_@' + pos;
          var cachedResult = cache[cacheKey];
          if (cachedResult) {
            pos = cachedResult.nextPos;
            return cachedResult.result;
          }

          var savedReportMatchFailures = reportMatchFailures;
          reportMatchFailures = false;
          var savedPos0 = pos;
          var result1 = [];
          var result3 = parse_whitespace();
          while (result3 !== null) {
            result1.push(result3);
            var result3 = parse_whitespace();
          }
          var result2 = result1 !== null
          ? (function(w) { return w.join(''); })(result1)
          : null;
          if (result2 !== null) {
            var result0 = result2;
          } else {
            var result0 = null;
            pos = savedPos0;
          }
          reportMatchFailures = savedReportMatchFailures;
          if (reportMatchFailures && result0 === null) {
            matchFailed("whitespace");
          }

          cache[cacheKey] = {
            nextPos: pos,
            result:  result0
          };
          return result0;
        }

        function parse_whitespace() {
          var cacheKey = 'whitespace@' + pos;
          var cachedResult = cache[cacheKey];
          if (cachedResult) {
            pos = cachedResult.nextPos;
            return cachedResult.result;
          }


          if (input.substr(pos).match(/^[ 	\n\r]/) !== null) {
            var result0 = input.charAt(pos);
            pos++;
          } else {
            var result0 = null;
            if (reportMatchFailures) {
              matchFailed("[ 	\\n\\r]");
            }
          }



          cache[cacheKey] = {
            nextPos: pos,
            result:  result0
          };
          return result0;
        }

        function buildErrorMessage() {
          function buildExpected(failuresExpected) {
            failuresExpected.sort();

            var lastFailure = null;
            var failuresExpectedUnique = [];
            for (var i = 0; i < failuresExpected.length; i++) {
              if (failuresExpected[i] !== lastFailure) {
                failuresExpectedUnique.push(failuresExpected[i]);
                lastFailure = failuresExpected[i];
              }
            }

            switch (failuresExpectedUnique.length) {
              case 0:
                return 'end of input';
              case 1:
                return failuresExpectedUnique[0];
              default:
                return failuresExpectedUnique.slice(0, failuresExpectedUnique.length - 1).join(', ')
              + ' or '
              + failuresExpectedUnique[failuresExpectedUnique.length - 1];
            }
          }

          var expected = buildExpected(rightmostMatchFailuresExpected);
          var actualPos = Math.max(pos, rightmostMatchFailuresPos);
          var actual = actualPos < input.length
          ? quote(input.charAt(actualPos))
          : 'end of input';

          return 'Expected ' + expected + ' but ' + actual + ' found.';
        }

        function computeErrorPosition() {
          /*
          * The first idea was to use |String.split| to break the input up to the
          * error position along newlines and derive the line and column from
          * there. However IE's |split| implementation is so broken that it was
          * enough to prevent it.
          */

          var line = 1;
          var column = 1;
          var seenCR = false;

          for (var i = 0; i <  rightmostMatchFailuresPos; i++) {
            var ch = input.charAt(i);
            if (ch === '\n') {
              if (!seenCR) { line++; }
              column = 1;
              seenCR = false;
            } else if (ch === '\r' | ch === '\u2028' || ch === '\u2029') {
              line++;
              column = 1;
              seenCR = true;
            } else {
              column++;
              seenCR = false;
            }
          }

          return { line: line, column: column };
        }



        var result = parseFunctions[startRule]();

        /*
        * The parser is now in one of the following three states:
        *
        * 1. The parser successfully parsed the whole input.
        *
        *    - |result !== null|
        *    - |pos === input.length|
        *    - |rightmostMatchFailuresExpected| may or may not contain something
        *
        * 2. The parser successfully parsed only a part of the input.
        *
        *    - |result !== null|
        *    - |pos < input.length|
        *    - |rightmostMatchFailuresExpected| may or may not contain something
        *
        * 3. The parser did not successfully parse any part of the input.
        *
        *   - |result === null|
        *   - |pos === 0|
        *   - |rightmostMatchFailuresExpected| contains at least one failure
        *
        * All code following this comment (including called functions) must
        * handle these states.
        */
        if (result === null || pos !== input.length) {
          var errorPosition = computeErrorPosition();
          throw new this.SyntaxError(
            buildErrorMessage(),
            errorPosition.line,
            errorPosition.column
          );
        }

        return result;
      },

      /* Returns the parser source code. */
      toSource: function() { return this._source; }
    };

    /* Thrown when a parser encounters a syntax error. */

    result.SyntaxError = function(message, line, column) {
      this.name = 'SyntaxError';
      this.message = message;
      this.line = line;
      this.column = column;
    };

    result.SyntaxError.prototype = Error.prototype;

    return result;
  })();

  MessageFormat.prototype.parse = function () {
    // Bind to itself so error handling works
    return mparser.parse.apply( mparser, arguments );
  };

  MessageFormat.prototype.precompile = function ( ast ) {
    var self = this,
        needOther = false,
        fp = {
      begin: 'function(d){\nvar r = "";\n',
      end  : "return r;\n}"
    };

    function interpMFP ( ast, data ) {
      // Set some default data
      data = data || {};
      var s = '', i, tmp, lastkeyname;

      switch ( ast.type ) {
        case 'program':
          return interpMFP( ast.program );
        case 'messageFormatPattern':
          for ( i = 0; i < ast.statements.length; ++i ) {
            s += interpMFP( ast.statements[i], data );
          }
          return fp.begin + s + fp.end;
        case 'messageFormatPatternRight':
          for ( i = 0; i < ast.statements.length; ++i ) {
            s += interpMFP( ast.statements[i], data );
          }
          return s;
        case 'messageFormatElement':
          data.pf_count = data.pf_count || 0;
          s += 'if(!d){\nthrow new Error("MessageFormat: No data passed to function.");\n}\n';
          if ( ast.output ) {
            s += 'r += d["' + ast.argumentIndex + '"];\n';
          }
          else {
            lastkeyname = 'lastkey_'+(data.pf_count+1);
            s += 'var '+lastkeyname+' = "'+ast.argumentIndex+'";\n';
            s += 'var k_'+(data.pf_count+1)+'=d['+lastkeyname+'];\n';
            s += interpMFP( ast.elementFormat, data );
          }
          return s;
        case 'elementFormat':
          if ( ast.key === 'select' ) {
            s += interpMFP( ast.val, data );
            s += 'r += (pf_' +
                 data.pf_count +
                 '[ k_' + (data.pf_count+1) + ' ] || pf_'+data.pf_count+'[ "other" ])( d );\n';
          }
          else if ( ast.key === 'plural' ) {
            s += interpMFP( ast.val, data );
            s += 'if ( pf_'+(data.pf_count)+'[ k_'+(data.pf_count+1)+' + "" ] ) {\n';
            s += 'r += pf_'+data.pf_count+'[ k_'+(data.pf_count+1)+' + "" ]( d ); \n';
            s += '}\nelse {\n';
            s += 'r += (pf_' +
                 data.pf_count +
                 '[ MessageFormat.locale["' +
                 self.fallbackLocale +
                 '"]( k_'+(data.pf_count+1)+' - off_'+(data.pf_count)+' ) ] || pf_'+data.pf_count+'[ "other" ] )( d );\n';
            s += '}\n';
          }
          return s;
        /* // Unreachable cases.
        case 'pluralStyle':
        case 'selectStyle':*/
        case 'pluralFormatPattern':
          data.pf_count = data.pf_count || 0;
          s += 'var off_'+data.pf_count+' = '+ast.offset+';\n';
          s += 'var pf_' + data.pf_count + ' = { \n';
          needOther = true;
          // We're going to simultaneously check to make sure we hit the required 'other' option.

          for ( i = 0; i < ast.pluralForms.length; ++i ) {
            if ( ast.pluralForms[ i ].key === 'other' ) {
              needOther = false;
            }
            if ( tmp ) {
              s += ',\n';
            }
            else{
              tmp = 1;
            }
            s += '"' + ast.pluralForms[ i ].key + '" : ' + interpMFP( ast.pluralForms[ i ].val,
          (function(){ var res = JSON.parse(JSON.stringify(data)); res.pf_count++; return res; })() );
          }
          s += '\n};\n';
          if ( needOther ) {
            throw new Error("No 'other' form found in pluralFormatPattern " + data.pf_count);
          }
          return s;
        case 'selectFormatPattern':

          data.pf_count = data.pf_count || 0;
          s += 'var off_'+data.pf_count+' = 0;\n';
          s += 'var pf_' + data.pf_count + ' = { \n';
          needOther = true;

          for ( i = 0; i < ast.pluralForms.length; ++i ) {
            if ( ast.pluralForms[ i ].key === 'other' ) {
              needOther = false;
            }
            if ( tmp ) {
              s += ',\n';
            }
            else{
              tmp = 1;
            }
            s += '"' + ast.pluralForms[ i ].key + '" : ' + interpMFP( ast.pluralForms[ i ].val,
              (function(){
                var res = JSON.parse( JSON.stringify( data ) );
                res.pf_count++;
                return res;
              })()
            );
          }
          s += '\n};\n';
          if ( needOther ) {
            throw new Error("No 'other' form found in selectFormatPattern " + data.pf_count);
          }
          return s;
        /* // Unreachable
        case 'pluralForms':
        */
        case 'string':
          return 'r += "' + MessageFormat.Utils.numSub(
            MessageFormat.Utils.escapeExpression( ast.val ),
            'k_' + data.pf_count + ' - off_' + ( data.pf_count - 1 ),
            data.pf_count
          ) + '";\n';
        default:
          throw new Error( 'Bad AST type: ' + ast.type );
      }
    }
    return interpMFP( ast );
  };

  MessageFormat.prototype.compile = function ( message ) {
    return (new Function( 'MessageFormat',
      'return ' +
        this.precompile(
          this.parse( message )
        )
    ))(MessageFormat);
  };


  if (typeof exports !== 'undefined') {
    if (typeof module !== 'undefined' && module.exports) {
      exports = module.exports = MessageFormat;
    }
    exports.MessageFormat = MessageFormat;
  }
  else if (typeof define === 'function' && define.amd) {
    define(function() {
      return MessageFormat;
    });
  }
  else {
    root['MessageFormat'] = MessageFormat;
  }

})( this );
